Borland Online And The Cobb Group Present:


September, 1994 - Vol. 1 No. 9

Automatically deallocating memory using smart pointers

One aspect of C and C++ programming that seems to always cause problems for beginners is dynamic memory allocation. If you declare an object within a function, you know it exists within stack memory and that the compiler will delete it when the function leaves scope. However, if you allocate memory from the heap by using malloc (for C programs) or new (for C++ programs), you're responsible for freeing or deleting the object.

In situations where the creation or destruction of an object occurs at consistent, logical places, this isn't a problem. However, if you write a function that contains multiple return statements or uses Exception Handling, destroying these dynamically allocated objects is much more complex.

In this article, we'll show how you can use smart pointer classes to simplify the cleanup of dynamically allocated objects. First, we'll describe the basic characteristics of smart pointers; then, we'll apply a smart pointer class in an example application.

What are smart pointers?

A smart pointer is a stack-based object (an object you create locally in a function) that controls your interaction with a heap-based object (an object you create via new). To create a smart pointer, you create a class that defines (for stack-based objects) the operations you'd normally perform via a pointer.

For example, if you want to create a smart pointer class for accessing character strings, you'll need to overload the operators you typically use with character pointers: for example, [] and =. (Obviously, the Borland C++ String class implements much of this functionality, and you'll probably want to use it for any production code. However, we'll use this example to demonstrate a simple smart pointer class.)

In memory, each smart pointer object will have a corresponding heap object, as shown in Figure A. However, since the smart pointer class overloads the standard pointer operators, the class lets you manipulate the heap object indirectly by passing those operator function calls to the corresponding heap object.


Figure A - The smart pointer object will pass all pointer operator calls to its heap object.

Because C++ programs always destroy stack-based objects when the enclosing function exits, you can control the allocation and deallocation of the character strings from the smart pointer's constructor and destructor. To see how using smart pointers can simplify the process of deleting dynamic objects, let's look at a simple example.

Dealing with returns

Maintaining someone else's code is a fact of life for most programmers. Unfortunately, not all programmers write perfect programs or even use good judgment in designing individual functions.

For example, few experienced programmers will write a function that contains multiple return statements, unless there's a very good reason to do so. One reason is that memory leaks can become difficult to locate when the logical flow of a function isn't obvious. Figure B contains a simple program that fails to delete the variable aptr if the user calls the program with no arguments.


Figure B - If a function contains multiple return statements, memory leaks can become difficult to find.

#include <iostream.h>
#include <string.h>

int main(int argc, char* argv[])
{
 char* aptr = new char[80];
 strcpy(aptr, argv[0]);
 char* bptr = 0;
 char* cptr = 0;
  
 if(argc >= 2)
 {
  bptr = new char[strlen(argv[1]) + 1];
  strcpy(bptr, argv[1]);
 }
 if(argc >= 3)
 {
  cptr = new char[strlen(argv[2]) + 1];
  strcpy(cptr, argv[2]);
 }

 cout << "Program - " << aptr << endl;

 switch(argc)
 {
  case 1:
   cout << "No arguments" << endl;
   return 1;

  case 3:
   cout << "Argument 2 - " << bptr << endl;

  default:
   cout << "Argument 1 - " << aptr << endl;
 }
 delete aptr;
 delete bptr;
 delete cptr;

 return 0;
}

To avoid the memory leak in this program, you'd need to add the line
delete aptr;

immediately above the statement

return 1;
 

Unfortunately, if you add additional return statements, you'll need to add the same line, and possibly others, in order to delete one of the other character strings. Now, let's rewrite this program by using a simple smart character pointer class to delete the dynamically allocated memory.

Many happy returns

To begin, launch the Borland C++ 3.1 DOS Integrated Development Environment (IDE). When the IDE's menu bar and desktop appear, choose New from the File menu and enter the code from Listing A in the empty data-file window that appears.


Listing A: smart.cpp

#include <iostream.h>
#include <string.h>

class smartCharPtr
{
  char* ptr;
 public:
  smartCharPtr() 
    { 
      ptr = new char[1];
      ptr[0] = '\0';
    }
  smartCharPtr(char* p) : ptr(p) 
    {}
  ~smartCharPtr() 
    { 
        cout << "deleting smartCharPtr for ";
        if (ptr)
            cout << ptr;
        else
            cout << " -";
        cout << endl;
        delete [] ptr;
    }
  operator char*()        // for strcpy()
    {   return ptr; }
  operator const char*()  // for cout
    {   return ptr; }
  smartCharPtr& operator=(const char* newPtr) 
    { 
        delete [] ptr;
        ptr = new char[strlen(newPtr) + 1];
        strcpy(ptr, newPtr);
        return *this; 
    }
};
int main(int argc, char* argv[])
{

    // store program name (argv[0])
    smartCharPtr aptr(new char[80]);
    strcpy(aptr, argv[0]);

    // set up two other pointers
    smartCharPtr bptr = 0;
    smartCharPtr cptr = 0;

    // give program name
    cout << "Program - " << aptr << endl;

    // save others (if present)
    if (argc > 1) {
        bptr = argv[1];
        cout << "Argument 1 = " << bptr << endl;
    }
    if (argc > 2) 
    {
        cptr = argv[2];
        cout << "Argument 2 = " << cptr << endl;
    }
    
    // if no args, get out
    if (argc == 1) {
        cout << "No Arguments" << endl;
        return 1;
    }

    return 0;

}

When you finish entering the code, choose Save from the File menu. When the Save File As dialog box appears, enter SMART.CPP as the file's name and click OK.

In the smartCharPtr class, we provide two constructors, a destructor, two conversion operators, and an overloaded assignment operator (=) function. Let's look at each of these functions individually.

The constructors allow us to build a smartCharPtr object with or without the corresponding pointer. However, you'll notice that if we create a smartCharPtr object without the pointer, we initialize the ptr data member to 0. This allows us to safely call delete on this pointer at a later time.

In the destructor, we first display a message that includes the character string (if the pointer is valid). Then, we delete the ptr data member pointer.

Next, we provide two conversion operator functions. The operator char*() function allows us to pass a smartCharPtr object to a function that's expecting a char pointer. The operator const char*() function performs the same task for functions that expect a const char pointer.

Finally, we overload the assignment operator with the operator=() function. The compiler will call this function whenever we try to initialize a smartCharPtr object with a char pointer.

In the body of the main() function, we simply added the smartCharPtr objects in place of the char pointers and changed the associated initialization code. Not only do these changes eliminate the memory leak that existed before, they also make the flow of the function easier to follow.

Running SMART.EXE

To build and run the application, choose Run from the Run menu. When Borland C++ finishes building the application, it will shell out to DOS, run the application, and then return to the IDE.

To view the results of the program, choose User Screen from the Window menu. When the results of the program appear, they should be similar to

Program - C:\BORLANDC\SMART.EXE
No arguments
deleting smartCharPtr for -
deleting smartCharPtr for -
deleting smartCharPtr for C:\BORLANDC\SMART.EXE

Next, press any key to return to the IDE screen and then choose Arguments... from the Run menu. In the Program Arguments dialog box, enter

1 2

and click OK.

Now, run the program again. When the program returns to the IDE, redisplay the user screen. This time, you should see

Program - C:\BORLANDC\SMART.EXE
Argument 1 - 1 
Argument 2 - 2
deleting smartCharPtr for - 2
deleting smartCharPtr for - 1
deleting smartCharPtr for C:\BORLANDC\SMART.EXE

You'll notice that the program deletes the smartCharPtr objects in reverse order. The program does this because it places (or pushes) variables on the program stack as you create them and then removes (or pops) them off the program stack in a first-in-last-out manner.

When you're finished viewing the output of the SMART.EXE program, press any key to return to the IDE screen. To quit the IDE, choose Quit from the File menu.

Other enhancements

Here, we're simply using a smart pointer to prevent memory leaks when we exit a function that uses heap objects. While this is an important use of smart pointer classes, it's by no means the only use for them.

One simple change we could make to the class is adding code that lets you use standard pointer syntax on the smart pointer object. For example, if you add the function

smarCharPtr& operator *()
{
  return *ptr;
} 

you can apply the dereference operator (*) to the smart pointer (even though it's a local object) in order to return the dereferenced pointer's value.

Templates make it simple

In the example above, we wrote a very simplistic smart pointer class for controlling the memory allocation for character pointers. However, for smart pointer classes to be really useful, you'll want to write a smart pointer template class.

By writing a generic smart pointer class as a template class, you'll be able to easily reuse the overloaded versions of many standard pointer operators (=, &, *, and so on). In a future issue, we'll show how you can write your own smart pointer template class, discuss some other uses for smart pointers, and describe some design decisions you'll need to make.

Conclusion

Designing programs that allocate and deallocate dynamic memory correctly can be an annoying and error-prone undertaking. By using smart pointers to automate these tasks, you can avoid memory leaks that would otherwise be difficult to overcome.

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.